#!/usr/bin/env perl

# port of the original mysql-agent snmp handler which will translate
# events for use with the net-snmpd pass_persist interface to be used
# on systems where AgentX support is unreliable

use strict;
use warnings;
use Data::Dumper;
use Unix::Syslog qw(:subs :macros);
use Getopt::Long qw(:config auto_help auto_version no_ignore_case);
use Pod::Usage;

use IO qw(Handle Select);

use MySQL::SNMP qw(fetch_mysql_data mysql_oidvals);

my %opt = (
    oid        => '1.3.6.1.4.1.20267.200.1',
    port       => 3306,
    refresh    => 300,
    master     => 1,
    slave      => 0,
    innodb     => 1,
    procs      => 0,
    host       => 'localhost',
    heartbeat  => '',
);

my %global_status       = ();
my $global_last_refresh = 0;
my $running = 0;
my $error   = 0;
# this will hold a table of conversion between numerical oids and oidnames
my %oids    = ();
my @ks;

our $stdin = IO::Handle->new_from_fd(fileno(STDIN), 'r');
our $stdout = IO::Handle->new_from_fd(fileno(STDOUT), 'w');
our $select = IO::Select->new($stdin);

$stdin->blocking(0);
$stdout->autoflush;

pp_run();

###
### Called automatically now and then
### Refreshes the $global_status and $global_variables
### caches.
###
sub pp_refresh_status {
    my $dsn      = shift;
    my $now      = time();

    # Check if we have been called quicker than once every $refresh
    if (($now - $global_last_refresh) < $opt{refresh}) {
        # if yes, do not do anything
        pp_dolog(LOG_DEBUG, "not refreshing: " . ($now - $global_last_refresh) . " < $opt{refresh}") if ($opt{verbose});
        return;
    }
    my ($status) = fetch_mysql_data($dsn, $opt{user}, $opt{password}, \%opt);
    if (defined($status) and ref $status eq 'HASH') {
        pp_dolog(LOG_DEBUG, "Setting error to 0") if ($opt{verbose});
        $error = 0;
        foreach my $key (keys %$status) {
            $global_status{$key} = $status->{$key};
        }
        pp_dolog(LOG_DEBUG, "Refreshed at $now " . (time() - $now)) if ($opt{verbose});
        print STDERR Dumper(\%global_status) if ($opt{debug});
    }
    else {
        pp_dolog(LOG_DEBUG, "Setting error to 1") if ($opt{verbose});
        $error = 1;
    }

    $global_last_refresh = $now;
    return;
}

sub pp_read_command {
    my (@ready,@ret);
    @ready = $select->can_read(0.25);
    return unless scalar(@ready) > 0;
    for my $fh (@ready) {
	push @ret, $fh->getlines;
	unless (defined($ret[0])) {
	    pp_shutdown();
	    exit 0;
	}
	next if ($ret[0] =~ m/^PING/);
	if (index($ret[0], 'get') == 0) {
	    # ensure we have the command and an oid number
	    while (scalar @ret < 2) {
		$fh->blocking(1);
		push @ret, $fh->getline;
		$fh->blocking(0);
	    }
	    next;
	}
	if (index($ret[0], 'set') == 0) {
	    while (scalar @ret < 3) {
		$fh->blocking(1);
		push @ret, $fh->getline;
		$fh->blocking(0);
	    }
	}
    }

    return \@ret;
}

sub pp_handle_cmd {
    my $buf = shift;
    while (my $line = shift(@$buf)) {
	chomp($line);
	if ($line eq 'PING') {
	    $stdout->print("PONG\n");
	    next;
	}
	elsif ($line eq 'set') {
	    # slurp off the remainder of the set command
	    my ($set_oid,$set_val) = splice(@$buf,0,2);
	    # tell the agent we cannot 'set' anything
	    $stdout->print("not-writable\n");
	    next;
	}
	elsif ($line eq 'get') {
	    my ($get_oid) = shift(@$buf);
	    chomp($get_oid);
	    $stdout->print(pp_set_value($get_oid));
	}
	elsif ($line eq 'getnext') {
	    my $get_oid = shift(@$buf);
	    chomp($get_oid);

	    unless (oidCmp($get_oid,$ks[$#ks]) == -1) {
		# got a request for a next oid outside of our range
		$stdout->print("NONE\n");
		return;
	    }

	    my @keys = @ks;
	    while (my $test_oid = shift @keys) {
		# skip temp oids not higher than the request
		next if (oidCmp($test_oid,$get_oid) < 1);
		my $out = pp_set_value($test_oid);
		# skip undefined oids while we have more to search
		next if $out eq "NONE\n" and scalar @keys > 0;
		$stdout->print($out);
		return;
	    }
	}
    }
}

sub pp_value {
    my $val = shift;
    # mask undef values to 0
    return 0 unless defined $val;
    if (ref $val eq 'Math::BigInt') {
	return $val->bstr();
    }
    if (ref $val ne '') {
	print STDERR "found unhandled ref: ", Dumper($val), "\n" if $opt{debug};
    }
    # hopefully safe to pass along directly now..
    return $val;
}

sub pp_shutdown {
    $running = 0;
    pp_dolog(LOG_INFO, "shutting down");
}

sub pp_set_value {
    my $get_oid = shift;

    print STDERR "validating current data for oid: $get_oid\n" if $opt{debug};
    unless (exists($oids{$get_oid}) and defined($global_status{$oids{$get_oid}->{name}})) {
	return("NONE\n");
    }

    my ($oid_name, $oid_type) = ($oids{$get_oid}->{name}, $oids{$get_oid}->{type});
    my $oid_val = pp_value($global_status{$oid_name}{value});
    return "$get_oid\n$oids{$get_oid}->{type}\n$oid_val\n";
}

sub oidCmp ($$;) {
    my ($a, $b) = @_;
    ($a, $b) = grep { s/^\.// } ($a, $b);
    return pack("w*", split(/\./,$a)) cmp pack("w*", split(/\./,$b));
}

sub byOidNum {
    local $a = $a;
    local $b = $b;
    return oidCmp($a, $b);
}

sub pp_dolog {
    my ($level, $msg) = @_;
    syslog($level, '%s', $msg);
}

sub VersionMessage {
    no warnings 'once';
    print "mysql-agent ", $MySQL::SNMP::Version, " by brice.figureau\@daysofwonder.com\n";
}

sub pp_run {
    GetOptions(
        \%opt,
        'host|h=s',
        'port|P=i',
        'user|u=s',
        'password|p=s',
        'config|c=s',
        'master|m!',
        'slave|s!',
	'slavebytes|bbm!',
        'innodb|i!',
        'oid|o=s',
	'procs|processes!',
        'refresh|r=i',
        'heartbeat|b=s',
        'man',
        'usage',
        'verbose|v+',
	'debug!',
        "version" => sub {VersionMessage()},
    ) or pod2usage(-verbose => 0);

    pod2usage(-verbose => 0) if $opt{usage};
    pod2usage(-verbose => 1) if $opt{help};
    pod2usage(-verbose => 2) if $opt{man};

    my $dsn = 'DBI:mysql:';
    if ($opt{config}) {
        $dsn .= "mysql_read_default_file=$opt{config}";
    }
    else {
        $dsn .= join(';', "host=$opt{host}", "port=$opt{port}");
    }

    openlog('mysql-ppsnmp', LOG_PID | LOG_PERROR, LOG_DAEMON);
    # close off STDERR if not connected to a tty (being run interactively)
    unless (-t STDERR) {
	open(STDERR, ">/dev/null");
    }

    # build the oids table
    foreach my $oidentry (@{ mysql_oidvals() }) {
	my $oidnum = ".$opt{oid}.$oidentry->{oid}.0";
	print STDERR "stashing oidnum $oidnum\n" if $opt{debug};
	$oids{$oidnum} = {name => $oidentry->{new_key},
			  type => $oidentry->{type},
			 };
	push @ks, $oidnum;
    }

    # this contains a lexicographycally sorted oids array
    @ks = sort byOidNum @ks;

    if ($opt{debug}) {
        foreach my $k (@ks) {
	    print STDERR "$k -> ", $oids{$k}->{'name'},"\n";
        }
    }


    # We need to perform a loop here waiting for snmp requests.     We
    # also check for new STATUS data.
    $SIG{'INT'}  = \&pp_shutdown;
    $SIG{'QUIT'} = \&pp_shutdown;
    $SIG{'TERM'} = \&pp_shutdown;
    $running = 1;

    # override the warn() signal handler so any stray errors
    # will get sent to the syslog output
    $SIG{'__WARN__'} = sub { pp_dolog(LOG_WARNING, $_[0]) };

    while ($running) {
        pp_refresh_status($dsn);
	# respond to any pending commands
	while (defined(my $cmd = pp_read_command())){
	    pp_handle_cmd($cmd);
	}
    }
    print STDERR "closing things down after signal\n" if $opt{debug};
    $select->remove($stdin);

    pp_dolog(LOG_INFO, "agent shutdown");
    exit 0;
}

__END__

=head1 NAME

    mysql-ppsnmp - report mysql statistics via Net-SNMP pass_persist interface

=head1 SYNOPSIS

    mysql-agent [options]

    -h HOST, --host=HOST      connect to MySQL DB on HOST
    -P PORT, --port=PORT      port to connect (default 3306)
    -u USER, --user=USER      use USER as user to connect to mysql
    -p PASS, --password=PASS  use PASS as password to connect to mysql
    -c FILE, --config=FILE    read mysql connection details from FILE
    -m, --master              check master
    -s, --slave               check slave
    -b, --heartbeat DB.TABLE  table for checking slave lag with mk-hearbeat
    -i, --innodb              read innodb settings
    -o OID, --oid=OID         registering OID
    -r INT, --refresh=INT     set refresh interval to INT (seconds)
    -v, --verbose             enable verbose logging to syslog
    -d, --debug               extra debug info to STDERR
    --procs, --processes      enable SHOW PROCESSLIST statistics

    -?, --help                display this help and exit
    --usage                   display detailed usage information
    --man                     display program man page
    -V, --version             output version information and exit

=head1 OPTIONS

=over 8

=item B<-h HOST, --host=HOST>

connect to MySQL DB on HOST

=item B<-P PORT, --port=PORT>

port to connect (default 3306)

=item B<-u USER, --user=USER>

use USER as user to connect to mysql

=item B<-p PASS, --password=PASS>

use PASS as password to connect to mysql

=item B<-c FILE, --config=FILE>

read mysql connection details from file FILE.

These should be stored in a section named [client]. Eg:

  [client]
  host=dbserver
  port=3306
  user=monitor
  password=secret

=item B<-m, --master>

check master

=item B<-s, --slave>

check slave

=item B<-b DB.TABLE, --heartbeat DB.TABLE>

specifies the table containing the mk-heartbeat timestamp for computing slave lag

=item B<-i, --innodb>

check innodb details

=item B<-o OID, --oid=OID>

registering OID

=item B<-r INT, --refresh=INT>

refresh interval in seconds

=item B<--procs, --processes>

enable tracking of 'SHOW PROCESSLIST' usage

=item B<-v, --verbose>

be verbose about what you do

=item B<-d, --debug>

extra debugging information to STDERR (will break the real pass_persist usage, only for interactive use)

=item B<--man>

Prints the manual page and exits.

=item B<--usage>

Prints detailed usage information and exits.

=item B<-?, --help>

Print a brief help message and exits.

=item B<-V, --version>

output version information and exit

=back

=head1 DESCRIPTION

B<mysql-ppsnmp> is a script ported from the logic of the B<mysql-agent>
which runs underneath a local snmpd daemon using the I<pass_persist> mode
to report statistics on a local or remote MySQL server.

=head1 EXAMPLE

In the snmpd.conf, use something like the following stanza to register
this script at the usual C<MYSQL-MIB.txt> location:

C<pass_persist .1.3.6.1.4.1.20267.200.1 /usr/share/snmp/mysql-ppsnmp -c /etc/snmp/my.cnf -h localhost -i --procs -r 60>

=cut
